<?php
namespace TwinPeaks\Core;

require_once 'CEscaper.php';


/**
 * Generic URI handler
 *
 */
abstract class CUri
{	
    /**
     * Character classes defined in RFC-3986
     */
    const CHAR_UNRESERVED = '\w\-\.~';
    const CHAR_GEN_DELIMS = ':\/\?#\[\]@';
    const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
    const CHAR_RESERVED   = ':\/\?#\[\]@!\$&\'\(\)\*\+,;=';

    /**
     * URI scheme
     *
     * @var string
     */
    protected $scheme;

    /**
     * URI userInfo part (usually user:password in HTTP URLs)
     *
     * @var string
     */
    protected $userInfo;

    /**
     * URI hostname
     *
     * @var string
     */
    protected $host;

    /**
     * URI port
     *
     * @var integer
     */
    protected $port;

    /**
     * URI path
     *
     * @var string
     */
    protected $path;

	/**
	 * @var array
	 */
	protected $queryArray;

    /**
     * URI fragment
     *
     * @var string
     */
    protected $fragment;

    /**
     * List of default ports per scheme
     *
     * Inheriting URI classes may set this, and the normalization methods will
     * automatically remove the port if it is equal to the default port for the
     * current scheme
     *
     * @var array
     */
    protected static $defaultPorts = array();

    /**
     * @var Escaper
     */
    protected static $escaper;
	
	/**
	 * @var string
	 */
	static protected $defaultScheme;
	
	/**
	 * @var string
	 */
	static protected $defaultHost;
	
	/**
	 * @return void
	 */
	static function setDefaultScheme($scheme)
	{
		static::$defaultScheme = $scheme;
	}
	
	/**
	 * @return string
	 */
	static function getDefaultScheme()
	{
		return static::$defaultScheme;
	}
	
	/**
	 * @return void
	 */
	static function setDefaultHost($hostname)
	{
		self::$defaultHost = $hostname;
	}
	
	/**
	 * @return string
	 */
	static function getDefaultHost()
	{
		return self::$defaultHost;
	}
    
    /**
     * Set Escaper instance
     *
     * @param  Escaper $escaper
     */
    public static function setEscaper(CEscaper $escaper)
    {
        static::$escaper = $escaper;
    }

    /**
     * Retrieve Escaper instance
     *
     * Lazy-loads one if none provided
     *
     * @return Escaper
     */
    public static function getEscaper()
    {
        if (null === static::$escaper) {
            static::setEscaper(new CEscaper());
        }
        return static::$escaper;
    }

    /**
     * Check if the URI is valid. Note that a relative URI may still be valid
     *
	 * @access public
     * @return boolean
     */
	function isValid()
    {
		if ($this->host)
		{
			if (strlen($this->path) > 0 && substr($this->path, 0, 1) != '/')
				return false;
			return true;
		}

		if ($this->userInfo || $this->port)
			return false;

		if ($this->path)
		{
			// Check path-only (no host) URI
			if (substr($this->path, 0, 2) == '//')
				return false;
			return true;
		}

		if (!(count($this->queryArray) || $this->fragment))
		{
			// No host, path, query or fragment - this is not a valid URI
			return false;
		}

		return true;
	}

    /**
     * Check if the URI is a valid relative URI
     *
	 * @final
	 * @access public
     * @return boolean
     */
    final function isValidRelative()
    {
		if ($this->scheme || $this->host || $this->userInfo || $this->port)
			return false;

		if ($this->path)
		{
			// Check path-only (no host) URI
			if (substr($this->path, 0, 2) == '//')
				return false;

			return true;
		}

		if (!(count($this->queryArray) || $this->fragment))
		{
			// No host, path, query or fragment - this is not a valid URI
			return false;
		}

		return true;
    }

    /**
     * Check if the URI is an absolute or relative URI
     *
	 * @final
	 * @access public
     * @return boolean
     */
	final function isAbsolute()
	{
		return ($this->scheme !== null);
	}

    /**
     * Compose the URI into a string
     *
     * @return string
     * @throws Exception\InvalidUriException
     */
	function toString()
	{
		if (!$this->isValid()) {
			if ($this->isAbsolute() || !$this->isValidRelative()) {
			throw new Exception\InvalidUriException(
				'URI is not valid and cannot be converted into a string'
				);
			}
		}

		$uri = '';

		if ($this->scheme !== null)
			$uri .= $this->scheme . ':';

		if ($this->host !== null)
		{
			$uri .= '//';
			
			if ($this->userInfo)
				$uri .= $this->userInfo . '@';
			
			$uri .= $this->host;
			
			if ($this->port)
				$uri .= ':' . $this->port;
		}

		if ($this->path)
			$uri .= self::encodePath($this->path);
		elseif ($this->host && (!count($this->queryArray) || $this->fragment))
			$uri .= '/';
		
		if (count($this->queryArray))
			$uri .= "?" . self::encodeQuery($this->queryArray);

		if ($this->fragment)
			$uri .= "#" . self::encodeQuery($this->fragment);

		return $uri;
	}

    /**
     * Normalize the URI
     *
     * Normalizing a URI includes removing any redundant parent directory or
     * current directory references from the path (e.g. foo/bar/../baz becomes
     * foo/baz), normalizing the scheme case, decoding any over-encoded
     * characters etc.
     *
     * Eventually, two normalized URLs pointing to the same resource should be
     * equal even if they were originally represented by two different strings
     *
	 * @final
	 * @access public
     * @return Uri
     */
	final function normalize()
	{
		if ($this->scheme)
			$this->scheme = static::normalizeScheme($this->scheme);

		if ($this->host)
			$this->host = static::normalizeHost($this->host);

		if ($this->port)
			$this->port = static::normalizePort($this->port, $this->scheme);

		if ($this->path)
			$this->path = static::normalizePath($this->path);

		if ($this->query)
			$this->query = static::normalizeQuery($this->query);

		if ($this->fragment)
			$this->fragment = static::normalizeFragment($this->fragment);

		// If path is empty (and we have a host), path should be '/'
		// Isn't this valid ONLY for HTTP-URI?
		if ($this->host && empty($this->path))
			$this->path = '/';

		return $this;
	}

    /**
     * Convert a relative URI into an absolute URI using a base absolute URI as
     * a reference.
     *
     * This is similar to merge() - only it uses the supplied URI as the
     * base reference instead of using the current URI as the base reference.
     *
     * Merging algorithm is adapted from RFC-3986 section 5.2
     * (@link http://tools.ietf.org/html/rfc3986#section-5.2)
     *
     * @param  Uri|string $baseUri
     * @return Uri
     */
	function resolve($baseUri)
	{
		// Ignore if URI is absolute
		if ($this->isAbsolute())
			return $this;

		if (is_string($baseUri))
			$baseUri = static::createFromString($baseUri);
		elseif (!$baseUri instanceof Uri)
		{
			//throw new Exception\InvalidArgumentException(
			//	'Provided base URI must be a string or a Uri object'
			//);
		}

		// Merging starts here...
		if ($this->getHost())
			$this->setPath(static::removePathDotSegments($this->getPath()));
		else
		{
			$basePath = $baseUri->getPath();
			$relPath  = $this->getPath();
			if (!$relPath)
			{
				$this->setPath($basePath);
				if (!$this->getQuery())
					$this->setQuery($baseUri->getQuery());
			}
			else
			{
				if (substr($relPath, 0, 1) == '/')
					$this->setPath(static::removePathDotSegments($relPath));
				else
				{
					if ($baseUri->getHost() && !$basePath)
						$mergedPath = '/';
					else
						$mergedPath = substr($basePath, 0, strrpos($basePath, '/') + 1);
					$this->setPath(static::removePathDotSegments($mergedPath . $relPath));
				}
			}

			// Set the authority part
			$this->setUserInfo($baseUri->getUserInfo());
			//var_dump($baseUri->getHost());exit;
			$this->setHost($baseUri->getHost());
			$this->setPort($baseUri->getPort());
		}

		$this->setScheme($baseUri->getScheme());
		return $this;
	}

    /**
     * Convert the link to a relative link by substracting a base URI
     *
     *  This is the opposite of resolving a relative link - i.e. creating a
     *  relative reference link from an original URI and a base URI.
     *
     *  If the two URIs do not intersect (e.g. the original URI is not in any
     *  way related to the base URI) the URI will not be modified.
     *
     * @param  Uri|string $baseUri
     * @return Uri
     */
    public function makeRelative($baseUri)
    {
        // Copy base URI, we should not modify it
        $baseUri = new static($baseUri);

        $this->normalize();
        $baseUri->normalize();

        $host     = $this->getHost();
        $baseHost = $baseUri->getHost();
        if ($host && $baseHost && ($host != $baseHost)) {
            // Not the same hostname
            return $this;
        }

        $port     = $this->getPort();
        $basePort = $baseUri->getPort();
        if ($port && $basePort && ($port != $basePort)) {
            // Not the same port
            return $this;
        }

        $scheme     = $this->getScheme();
        $baseScheme = $baseUri->getScheme();
        if ($scheme && $baseScheme && ($scheme != $baseScheme)) {
            // Not the same scheme (e.g. HTTP vs. HTTPS)
            return $this;
        }

        // Remove host, port and scheme
        $this->setHost(null)
             ->setPort(null)
             ->setScheme(null);

        // Is path the same?
        if ($this->getPath() == $baseUri->getPath()) {
            $this->setPath('');
            return $this;
        }

        $pathParts = preg_split('|(/)|', $this->getPath(), null,
                                PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
        $baseParts = preg_split('|(/)|', $baseUri->getPath(), null,
                                PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

        // Get the intersection of existing path parts and those from the
        // provided URI
        $matchingParts = array_intersect_assoc($pathParts, $baseParts);

        // Loop through the matches
        foreach ($matchingParts as $index => $segment) {
            // If we skip an index at any point, we have parent traversal, and
            // need to prepend the path accordingly
            if ($index && !isset($matchingParts[$index - 1])) {
                array_unshift($pathParts, '../');
                continue;
            }

            // Otherwise, we simply unset the given path segment
            unset($pathParts[$index]);
        }

        // Reset the path by imploding path segments
        $this->setPath(implode($pathParts));

        return $this;
    }
	
	/**
	 * @final
	 * @access public
	 * @return CUri
	 */
	final function getRelativeUri($noQuery = false)
	{
		$relUri  = $this->getCopy();
		$relUri->setScheme(null)
			->setHost(null)
			->setPort(null)
		;
		
		if ($noQuery)
			$relUri->setQuery(null);
		
		return $relUri;
	}
	
	/**
	 * @see CUri::getRelativeUri
	 * @final
	 * @access public
	 * @return CUri
	 */
	final function getRelativeUriNoQuery()
	{
		return $this->getRelativeUri(true);
	}
	
	/**
	 * @return string
	 */
	function getQueryParam()
	{
		
	}

    /**
     * Get the scheme part of the URI
     *
     * @return string|null
     */
    public function getScheme()
    {
        return $this->scheme;
    }

    /**
     * Get the User-info (usually user:password) part
     *
     * @return string|null
     */
    public function getUserInfo()
    {
        return $this->userInfo;
    }

	/**
	 * Get the URI host
	 *
	 * @return string|null
	 */
	function getHost()
	{
		return $this->host;
	}

    /**
     * Get the URI port
     *
     * @return integer|null
     */
    public function getPort()
    {
        return $this->port;
    }

    /**
     * Get the URI path
     *
     * @return string|null
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Return the query string as an associative array of key => value pairs
     *
     * This is an extension to RFC-3986 but is quite useful when working with
     * most common URI types
     *
     * @return array
     */
    public function getQueryAsArray()
    {
        $query = array();
        if ($this->query) {
            parse_str($this->query, $query);
        }

        return $query;
    }

    /**
     * Get the URI fragment
     *
     * @return string|null
     */
    public function getFragment()
    {
        return $this->fragment;
    }

    /**
     * Set the URI scheme
     *
     * @param  string $scheme
     * @return Uri
     */
    function setScheme($scheme)
    {
        $this->scheme = $scheme;
        return $this;
    }

    /**
     * Set the URI User-info part (usually user:password)
     *
     * @param  string $userInfo
     * @return Uri
     * @throws Exception\InvalidUriPartException If the schema definition
     * does not have this part
     */
    public function setUserInfo($userInfo)
    {
        $this->userInfo = $userInfo;
        return $this;
    }

    /**
     * Set the URI host
     *
     * @param  string $host
     * @return Uri
     */
    public function setHost($host)
    {
        $this->host = $host;
        return $this;
    }

    /**
     * Set the port part of the URI
     *
     * @param  integer $port
     * @return Uri
     */
    public function setPort($port)
    {
        $this->port = $port;
        return $this;
    }

    /**
     * Set the path
     *
     * @param  string $path
     * @return Uri
     */
    public function setPath($path)
    {
        $this->path = $path;
        return $this;
    }
	
    /**
     * Get the URI query
     *
	 * @access public
     * @return string|null
     */
    final function getQuery()
    {
       return $this->buildQueryFromArray($this->queryArray);
    }	

    /**
     * Set the query string
     *
     * If an array is provided, will encode this array of parameters into a
     * query string. Array values will be represented in the query string using
     * PHP's common square bracket notation.
     *
     * @param  string|array $query
	 * @final
	 * @access public
     * @return Uri
     */
	final function setQuery($query)
	{
		if (empty($query))
			$this->queryArray = [];
		else if (is_array($query))
			$this->queryArray = $query;
		else
			parse_str($query, $this->queryArray);

		return $this;
	}
	
	/**
	 * @param array $queryArray
	 * @access protected
	 * @return string
	 */
	protected function buildQueryFromArray($queryArray)
	{
		return http_build_query($queryArray, null, null, PHP_QUERY_RFC3986);
	}
	
	/**
	 * @return mixed
	 */
	function getQueryArg($name)
	{
		return isset($this->queryArray[$name]) ?
			$this->queryArray[$name] : null;
	}
	
	/**
	 * 
	 */
	function hasQueryArg($name)
	{
		return isset($this->queryArray[$name]);
	}
	
	/**
	 * 
	 */
	function setQueryArg($name, $value)
	{
		$this->queryArray[$name] = $value;
	}
	
	/**
	 * 
	 */
	function removeQueryArg($name)
	{
		unset($this->queryArray[$name]);
	}

    /**
     * Set the URI fragment part
     *
     * @param  string $fragment
     * @return Uri
     * @throws Exception\InvalidUriPartException If the schema definition
     * does not have this part
     */
    public function setFragment($fragment)
    {
        $this->fragment = $fragment;
        return $this;
    }

    /**
     * Magic method to convert the URI to a string
     *
     * @return string
     */
    public function __toString()
    {
        try
		{
            return $this->toString();
        }
		catch (\Exception $e)
		{
            return '';
        }
    }

    /**
     * Check that the userInfo part of a URI is valid
     *
     * @param  string $userInfo
     * @return boolean
     */
    public static function validateUserInfo($userInfo)
    {
        $regex = '/^(?:[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ':]+|%[A-Fa-f0-9]{2})*$/';
        return (boolean) preg_match($regex, $userInfo);
    }

    /**
     * Validate the path
     *
     * @param  string $path
     * @return boolean
     */
    public static function validatePath($path)
    {
        $pchar   = '(?:[' . self::CHAR_UNRESERVED . ':@&=\+\$,]+|%[A-Fa-f0-9]{2})*';
        $segment = $pchar . "(?:;{$pchar})*";
        $regex   = "/^{$segment}(?:\/{$segment})*$/";
        return (boolean) preg_match($regex, $path);
    }

    /**
     * Check if a URI query or fragment part is valid or not
     *
     * Query and Fragment parts are both restricted by the same syntax rules,
     * so the same validation method can be used for both.
     *
     * You can encode a query or fragment part to ensure it is valid by passing
     * it through the encodeQueryFragment() method.
     *
     * @param  string $input
     * @return boolean
     */
    public static function validateQueryFragment($input)
    {
        $regex = '/^(?:[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ':@\/\?]+|%[A-Fa-f0-9]{2})*$/';
        return (boolean) preg_match($regex, $input);
    }

    /**
     * URL-encode the user info part of a URI
     *
     * @param  string $userInfo
     * @return string
     * @throws Exception\InvalidArgumentException
     */
    public static function encodeUserInfo($userInfo)
    {
        if (!is_string($userInfo)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Expecting a string, got %s',
                (is_object($userInfo) ? get_class($userInfo) : gettype($userInfo))
            ));
        }

        $regex   = '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:]|%(?![A-Fa-f0-9]{2}))/';
        $escaper = static::getEscaper();
        $replace = function ($match) use ($escaper) {
            return $escaper->escapeUrl($match[0]);
        };

        return preg_replace_callback($regex, $replace, $userInfo);
    }

    /**
     * Encode the path
     *
     * Will replace all characters which are not strictly allowed in the path
     * part with percent-encoded representation
     *
     * @param  string $path
     * @throws Exception\InvalidArgumentException
     * @return string
     */
    public static function encodePath($path)
    {
		$regex   = '/(?:[^' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/';
		$escaper = static::getEscaper();
		$replace = function ($match) use ($escaper) {
			return $escaper->escapeUrl($match[0]);
		};

		return preg_replace_callback($regex, $replace, $path);
    }

    /**
     * URL-encode a query string or fragment based on RFC-3986 guidelines.
     *
     * Note that query and fragment encoding allows more unencoded characters
     * than the usual rawurlencode() function would usually return - for example
     * '/' and ':' are allowed as literals.
     *
     * @param  string $input
	 * @static
	 * @access public
     * @return string
     */
    static function encodeQuery($query)
    {
		$queryString = '';
		if (is_array($query))
			foreach ($query as $key => $value)
			{
				$queryString .= rawurlencode($key) . '=' . rawurlencode($value);
				$queryString .= '&';
			}
		else
			$queryString = rawurlencode($query);
		
		$queryString = rtrim($queryString, '&');
		return $queryString;
    }

    /**
     * Extract only the scheme part out of a URI string.
     *
     * This is used by the parse() method, but is useful as a standalone public
     * method if one wants to test a URI string for it's scheme before doing
     * anything with it.
     *
     * Will return the scheme if found, or NULL if no scheme found (URI may
     * still be valid, but not full)
     *
     * @param  string $uriString
     * @throws Exception\InvalidArgumentException
     * @return string|null
     */
    public static function parseScheme($uriString)
    {
        if (! is_string($uriString)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Expecting a string, got %s',
                (is_object($uriString) ? get_class($uriString) : gettype($uriString))
            ));
        }

        if (preg_match('/^([A-Za-z][A-Za-z0-9\.\+\-]*):/', $uriString, $match)) {
            return $match[1];
        }

        return null;
    }

    /**
     * Remove any extra dot segments (/../, /./) from a path
     *
     * Algorithm is adapted from RFC-3986 section 5.2.4
     * (@link http://tools.ietf.org/html/rfc3986#section-5.2.4)
     *
     * @todo   consider optimizing
     *
     * @param  string $path
     * @return string
     */
    public static function removePathDotSegments($path)
    {
        $output = '';

        while ($path) {
            if ($path == '..' || $path == '.') {
                break;
            }

            switch (true) {
                case ($path == '/.'):
                    $path = '/';
                    break;
                case ($path == '/..'):
                    $path   = '/';
                    $output = substr($output, 0, strrpos($output, '/', -1));
                    break;
                case (substr($path, 0, 4) == '/../'):
                    $path   = '/' . substr($path, 4);
                    $output = substr($output, 0, strrpos($output, '/', -1));
                    break;
                case (substr($path, 0, 3) == '/./'):
                    $path = substr($path, 2);
                    break;
                case (substr($path, 0, 2) == './'):
                    $path = substr($path, 2);
                    break;
                case (substr($path, 0, 3) == '../'):
                    $path = substr($path, 3);
                    break;
                default:
                    $slash = strpos($path, '/', 1);
                    if ($slash === false) {
                        $seg = $path;
                    } else {
                        $seg = substr($path, 0, $slash);
                    }

                    $output .= $seg;
                    $path    = substr($path, strlen($seg));
                    break;
            }
        }

        return $output;
    }

	/**
	 * Merge a base URI and a relative URI into a new URI object
	 *
	 * This convenience method wraps ::resolve() to allow users to quickly
	 * create new absolute URLs without the need to instantiate and clone
	 * URI objects.
	 *
	 * If objects are passed in, none of the passed objects will be modified.
	 *
	 * @param  Uri|string $baseUri
	 * @param  Uri|string $relativeUri
	 * @return Uri
	 */
	static function merge($baseUri, $relativeUri)
	{
		if (is_string($baseUri))
			$baseUri = self::createFromString($baseUri);
		if (is_string($relativeUri))
			$relativeUri = self::createFromString($relativeUri);

		return $relativeUri->resolve($baseUri);
	}

    /**
     * Check if an address is a valid registered name (as defined by RFC-3986) address
     *
     * @param  string $host
     * @return boolean
     */
    protected static function isValidRegName($host)
    {
        $regex = '/^(?:[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ':@\/\?]+|%[A-Fa-f0-9]{2})+$/';
        return (bool) preg_match($regex, $host);
    }

    /**
     * Part normalization methods
     *
     * These are called by normalize() using static::_normalize*() so they may
     * be extended or overridden by extending classes to implement additional
     * scheme specific normalization rules
     */

    /**
     * Normalize the scheme
     *
     * Usually this means simply converting the scheme to lower case
     *
     * @param  string $scheme
     * @return string
     */
    protected static function normalizeScheme($scheme)
    {
        return strtolower($scheme);
    }

    /**
     * Normalize the host part
     *
     * By default this converts host names to lower case
     *
     * @param  string $host
     * @return string
     */
    protected static function normalizeHost($host)
    {
        return strtolower($host);
    }

    /**
     * Normalize the port
     *
     * If the class defines a default port for the current scheme, and the
     * current port is default, it will be unset.
     *
     * @param  integer $port
     * @param  string  $scheme
     * @return integer|null
     */
    protected static function normalizePort($port, $scheme = null)
    {
        if ($scheme
            && isset(static::$defaultPorts[$scheme])
            && ($port == static::$defaultPorts[$scheme])
        ) {
            return null;
        }

        return $port;
    }

    /**
     * Normalize the path
     *
     * This involves removing redundant dot segments, decoding any over-encoded
     * characters and encoding everything that needs to be encoded and is not
     *
     * @param  string $path
     * @return string
     */
    protected static function normalizePath($path)
    {
        $path = self::encodePath(
            self::decodeUrlEncodedChars(
                self::removePathDotSegments($path),
                '/[' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]/'
            )
        );

        return $path;
    }

    /**
     * Normalize the query part
     *
     * This involves decoding everything that doesn't need to be encoded, and
     * encoding everything else
     *
     * @param  string $query
     * @return string
     */
    protected static function normalizeQuery($query)
    {
        $query = self::encodeQueryFragment(
            self::decodeUrlEncodedChars(
                $query,
                '/[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]/'
            )
        );

        return $query;
    }

    /**
     * Normalize the fragment part
     *
     * Currently this is exactly the same as normalizeQuery().
     *
     * @param  string $fragment
     * @return string
     */
    protected static function normalizeFragment($fragment)
    {
        return static::normalizeQuery($fragment);
    }

    /**
     * Decode all percent encoded characters which are allowed to be represented literally
     *
     * Will not decode any characters which are not listed in the 'allowed' list
     *
     * @param string $input
     * @param string $allowed Pattern of allowed characters
     * @return mixed
     */
    protected static function decodeUrlEncodedChars($input, $allowed = '')
    {
        $decodeCb = function($match) use ($allowed) {
            $char = rawurldecode($match[0]);
            if (preg_match($allowed, $char)) {
                return $char;
            }
            return strtoupper($match[0]);
        };

        return preg_replace_callback('/%[A-Fa-f0-9]{2}/', $decodeCb, $input);
    }
	
	/**
	 * Parse a URI string.
	 *
	 * @param string $uri
	 * @access public
	 * @return CUri
	 */
	final static function createFromString($uri)
	{
		$uriInstance = new static();
		
		// Capture scheme
		if (($scheme = self::parseScheme($uri)) !== null)
		{
			$uriInstance->setScheme($scheme);
			$uri = substr($uri, strlen($scheme) + 1);
		}

		// Capture authority part
		if (preg_match('|^//([^/\?#]*)|', $uri, $match))
		{
			$authority = $match[1];
			$uri = substr($uri, strlen($match[0]));

			// Split authority into userInfo and host
			if (strpos($authority, '@') !== false)
			{
				// The userInfo can also contain '@' symbols; split $authority
				// into segments, and set it to the last segment.
				$segments  = explode('@', $authority);
				$authority = array_pop($segments);
				$userInfo  = implode('@', $segments);
				unset($segments);
				$uriInstance->setUserInfo($userInfo);
			}

			$nMatches = preg_match('/:[\d]{1,5}$/', $authority, $matches);
			if ($nMatches === 1)
			{
				$portLength = strlen($matches[0]);
				$port = substr($matches[0], 1);

				$uriInstance->setPort((int) $port);
				$authority = substr($authority, 0, -$portLength);
			}
			$uriInstance->setHost($authority);
		}

		if (!$uri)
			return $uriInstance;
		
		// Capture the path
		if (preg_match('|^[^\?#]*|', $uri, $match))
		{
			$uriInstance->setPath($match[0]);
			$uri = substr($uri, strlen($match[0]));
		}

		if (!$uri)
			return $uriInstance;

		// Capture the query
		if (preg_match('|^\?([^#]*)|', $uri, $match))
		{
			$uriInstance->setQuery($match[1]);
			$uri = substr($uri, strlen($match[0]));
		}
		
		if (!$uri)
			return $uriInstance;

		// All that's left is the fragment
		if ($uri && substr($uri, 0, 1) == '#')
			$uriInstance->setFragment(substr($uri, 1));

		return $uriInstance;
	}

	/**
	 * @final
	 * @access public
	 * @return CUri
	 */
	final function getCopy()
	{
		return clone $this;
	}	
}
